package com.gjy.mongod.codec;

import com.google.common.collect.Lists;
import org.bson.BsonReader;
import org.bson.BsonType;
import org.bson.BsonWriter;
import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.types.Decimal128;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Timestamp;
import java.time.*;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author 宫静雨
 * @version 1.0
 * @since 2023-10-16 14:33:12
 */
public class GenericProvider implements CodecProvider {

    private static final Logger log = LoggerFactory.getLogger(GenericProvider.class);

    @Override
    public <T> Codec<T> get(Class<T> cls, CodecRegistry codecRegistry) {

        try {
            log.info("cls = {}", cls.getName());
            if (Objects.nonNull(cls)) {
                return new GenericCodec<>(cls);
            }
            return null;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static class GenericCodec<T> implements Codec<T> {

        // 基本类型
        private static final ArrayList<? extends Class<? extends Serializable>> BASIC_KIND =
                Lists.newArrayList(int.class, byte.class, short.class, long.class, float.class, double.class, char.class, boolean.class);
        // 包装类型
        private static final ArrayList<? extends Class<? extends Serializable>> PACKAGES_KIND =
                Lists.newArrayList(Integer.class, Byte.class, Short.class, Long.class, Float.class, Double.class, Character.class, Boolean.class);
        // 日期类型
        private static final ArrayList<? extends Class<? extends Serializable>> DATE_KIND =
                Lists.newArrayList(Date.class, Calendar.class, Timestamp.class, LocalDate.class, LocalDateTime.class, LocalTime.class,
                        Duration.class, Period.class, Instant.class);
        // 特殊数值类型
        private static final ArrayList<? extends Class<? extends Serializable>> NUMBER_KIND =
                Lists.newArrayList(BigDecimal.class, BigInteger.class);
        // 集合类型
        private static final ArrayList<Class<? extends Collection>> LIST_KIND =
                Lists.newArrayList(List.class, ArrayList.class, Set.class, HashSet.class);

        private final Class<T> clazz;

        public GenericCodec(Class<T> clazz) {
            this.clazz = clazz;
        }

        @Override
        public T decode(BsonReader reader, DecoderContext decoderContext) {
            try {
                T t = clazz.newInstance();
                Field[] fields = clazz.getDeclaredFields();
                Map<String, Field> map = fieldToMap(fields);

                reader.readStartDocument();
                while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                    String s = reader.readName();
                    if (s.equals("_id")) {
                        map.get("id").set(t, reader.readInt64());
                    } else {
                        String fieldTypeName = map.get(s).getType().getSimpleName();
                        switch (fieldTypeName) {
                            case "Long":
                                map.get(s).set(t, reader.readInt64());
                                break;
                            case "Integer":
                                map.get(s).set(t, reader.readInt32());
                                break;
                            case "String":
                                map.get(s).set(t, reader.readString());
                                break;
                            case "Double":
                                map.get(s).set(t, reader.readDouble());
                                break;
                            case "BigDecimal":
                                map.get(s).set(t, BigDecimal.valueOf(reader.readDouble()));
                                break;
                            default:
                                break;
                        }
                    }
                }
                reader.readEndDocument();
                return t;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        private Map<String, Field> fieldToMap(Field[] fields) {
            return Arrays.stream(fields)
                    .peek(f -> f.setAccessible(true))
                    .collect(Collectors.toMap(Field::getName, f -> f));
        }

        /*@Override
        public void encode(BsonWriter writer, T t, EncoderContext context) {
            writer.writeStartDocument();
            try {
                Field[] fields = clazz.getDeclaredFields();
                for (Field field : fields) {
                    field.setAccessible(true);
                    if (field.isAnnotationPresent(MongoId.class)) {
                        writer.writeInt64("_id", ((Long) field.get(t)));
                    } else {
                        String fieldName = field.getName();
                        String fieldTypeName = field.getType().getSimpleName();
                        switch (fieldTypeName) {
                            case "Long":
                                writer.writeInt64(fieldName, ((Long) field.get(t)));
                                break;
                            case "Integer":
                                writer.writeInt32(fieldName, ((Integer) field.get(t)));
                                break;
                            case "String":
                                writer.writeString(fieldName, field.get(t).toString());
                                break;
                            case "Double":
                                writer.writeDouble(fieldName, ((Double) field.get(t)));
                                break;
                            case "BigDecimal":
                                writer.writeDouble(fieldName, ((BigDecimal) field.get(t)).doubleValue());
                                break;
                            default:
                                log.info("未识别: {},{}", fieldName, field.get(t));
                                break;
                        }
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            writer.writeEndDocument();
        }*/

        @Override
        public void encode(BsonWriter writer, T t, EncoderContext context) {
            encode(writer, t);
        }

        public void encode(BsonWriter writer, T t) {
            writer.writeStartDocument();
            Field[] fields = clazz.getDeclaredFields();

            for (Field field : fields) {
                field.setAccessible(true);

                if (BASIC_KIND.contains(field.getType())) { // 基本类型处理
                    basicKind(writer, t, field);
                } else if (PACKAGES_KIND.contains(field.getType())) { // 包装类型
                    packagesKind(writer, t, field);
                } else if (DATE_KIND.contains(field.getType())) { // 日期类型
                    dateKind(writer, t, field);
                } else if (NUMBER_KIND.contains(field.getType())) { // 特殊数值类型
                    numberKind(writer, t, field);
                } else if (LIST_KIND.contains(field.getType())) { // 集合类型
                    listKind(writer, t, field);
                } else if (String.class == field.getType()) { // 字符串类型
                    stringKind(writer, t, field);
                } else { // 自定义实体类型
                    try {
                        encode(writer, field.get(t), field.getType(), field.getName());
                    } catch (IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            writer.writeEndDocument();
        }

        public void encode(BsonWriter writer, Object t, Class<?> cls, String fieldName) {
            writer.writeStartDocument(fieldName);
            Field[] fields = cls.getDeclaredFields();

            for (Field field : fields) {
                field.setAccessible(true);

                try {
                    if (BASIC_KIND.contains(field.getType())) { // 基本类型处理
                        basicKindForList(writer, field.get(t), field.getType().getSimpleName());
                    } else if (PACKAGES_KIND.contains(field.getType())) { // 包装类型
                        packagesKindForList(writer, field.get(t), field.getType().getName());
                    } else if (DATE_KIND.contains(field.getType())) { // 日期类型
                        dateKindForList(writer, field.get(t), field.getType().getName());
                    } else if (NUMBER_KIND.contains(field.getType())) { // 特殊数值类型
                        numberKindForList(writer, field.get(t), field.getType().getName());
                    } else if (String.class == field.getType()) { // 字符串类型
                        writer.writeString(String.valueOf(field.get(t)));
                    } else { // 自定义实体类型
                        encode(writer, field.get(t), field.getType(), field.getName());
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
            writer.writeEndDocument();
        }

        private void basicKind(BsonWriter writer, T t, Field field) {
            String fieldType = field.getType().getSimpleName();
            String fieldName = field.getName();

            try {
                switch (fieldType) {
                    case "byte":
                        writer.writeInt32(fieldName, field.getByte(t));
                        break;
                    case "short":
                        writer.writeInt32(fieldName, field.getShort((t)));
                        break;
                    case "int":
                        writer.writeInt32(fieldName, field.getInt(t));
                        break;
                    case "long":
                        writer.writeInt64(fieldName, field.getLong(t));
                        break;
                    case "float":
                        writer.writeDouble(fieldName, field.getFloat(t));
                        break;
                    case "double":
                        writer.writeDouble(fieldName, field.getDouble(t));
                        break;
                    case "char":
                        writer.writeString(fieldName, String.valueOf(field.getChar(t)));
                        break;
                    case "boolean":
                        writer.writeBoolean(fieldType, field.getBoolean(t));
                        break;
                    default:
                        log.info("基本类型解析错误");
                        break;
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        private void basicKindForList(BsonWriter writer, Object o, String fieldType) {
            try {
                switch (fieldType) {
                    case "byte":
                        writer.writeInt32(((byte) o));
                        break;
                    case "short":
                        writer.writeInt32(((short) o));
                        break;
                    case "int":
                        writer.writeInt32(((int) o));
                        break;
                    case "long":
                        writer.writeInt64(((long) o));
                        break;
                    case "float":
                        writer.writeDouble(((float) o));
                        break;
                    case "double":
                        writer.writeDouble(((double) o));
                        break;
                    case "char":
                        writer.writeString(String.valueOf(((char) o)));
                        break;
                    case "boolean":
                        writer.writeBoolean(((boolean) o));
                        break;
                    default:
                        log.info("基本类型解析错误");
                        break;
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        private void packagesKindForList(BsonWriter writer, Object o, String fieldType) {
            try {
                switch (fieldType) {
                    case "java.lang.Byte":
                        writer.writeInt32(((Byte) o));
                        break;
                    case "java.lang.Short":
                        writer.writeInt32(((Short) o));
                        break;
                    case "java.lang.Integer":
                        writer.writeInt32(((Integer) o));
                        break;
                    case "java.lang.Long":
                        writer.writeInt64(((Long) o));
                        break;
                    case "java.lang.Float":
                        writer.writeDouble(((Float) o));
                        break;
                    case "java.lang.Double":
                        writer.writeDouble(((Double) o));
                        break;
                    case "java.lang.String":
                        writer.writeString(String.valueOf(o));
                        break;
                    case "java.lang.Boolean":
                        writer.writeBoolean(((Boolean) o));
                        break;
                    default:
                        log.info("集合基本类型解析错误");
                        break;
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        private void packagesKind(BsonWriter writer, T t, Field field) {
            String fieldType = field.getType().getSimpleName();
            String fieldName = field.getName();

            try {
                switch (fieldType) {
                    case "Byte":
                        writer.writeInt32(fieldName, ((Byte) field.get(t)));
                        break;
                    case "Short":
                        writer.writeInt32(fieldName, ((Short) field.get((t))));
                        break;
                    case "Integer":
                        writer.writeInt32(fieldName, ((Integer) field.get(t)));
                        break;
                    case "Long":
                        writer.writeInt64(fieldName, ((Long) field.get(t)));
                        break;
                    case "Float":
                        writer.writeDouble(fieldName, ((Float) field.get(t)));
                        break;
                    case "Double":
                        writer.writeDouble(fieldName, ((Double) field.get(t)));
                        break;
                    case "Character":
                        writer.writeString(fieldName, String.valueOf(field.get(t)));
                        break;
                    case "Boolean":
                        writer.writeBoolean(fieldType, ((Boolean) field.get(t)));
                        break;
                    default:
                        log.info("包装类型解析错误");
                        break;
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        private void dateKind(BsonWriter writer, T t, Field field) {
            String fieldType = field.getType().getSimpleName();
            String fieldName = field.getName();

            try {
                switch (fieldType) {
                    case "Date":
                        writer.writeDateTime(fieldName, ((Date) field.get(t)).toInstant().minus(Duration.ofHours(8)).toEpochMilli());
                        break;
                    case "LocalDate":
                        writer.writeString(fieldName, ((LocalDate) field.get(t)).toString());
                        break;
                    case "LocalTime":
                        writer.writeString(fieldName, ((LocalTime) field.get(t)).toString());
                        break;
                    case "LocalDateTime":
                        writer.writeString(fieldName, ((LocalDateTime) field.get(t)).toString());
                        break;
                    default:
                        log.info("特殊数值类型解析错误");
                        break;
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        private void dateKindForList(BsonWriter writer, Object o, String fieldType) {
            try {
                switch (fieldType) {
                    case "java.util.Date":
                        writer.writeDateTime(((Date) o).toInstant().minus(Duration.ofHours(8)).toEpochMilli());
                        break;
                    case "java.time.LocalDate":
                        writer.writeString(((LocalDate) o).toString());
                        break;
                    case "java.time.LocalTime":
                        writer.writeString(((LocalTime) o).toString());
                        break;
                    case "java.time.LocalDateTime":
                        writer.writeString(((LocalDateTime) o).toString());
                        break;
                    default:
                        log.info("集合特殊数值类型解析错误");
                        break;
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        private void numberKind(BsonWriter writer, T t, Field field) {
            String fieldType = field.getType().getSimpleName();
            String fieldName = field.getName();

            try {
                switch (fieldType) {
                    case "BigInteger":
                        writer.writeInt64(fieldName, ((BigInteger) field.get(t)).longValue());
                        break;
                    case "BigDecimal":
                        writer.writeDecimal128(fieldName, new Decimal128(((BigDecimal) field.get(t))));
                        break;
                    default:
                        log.info("特殊数值类型解析错误");
                        break;
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        private void numberKindForList(BsonWriter writer, Object o, String fieldType) {
            try {
                switch (fieldType) {
                    case "java.math.BigInteger":
                        writer.writeInt64(((BigInteger) o).longValue());
                        break;
                    case "java.math.BigDecimal":
                        writer.writeDecimal128(new Decimal128(((BigDecimal) o)));
                        break;
                    default:
                        log.info("特殊数值类型解析错误");
                        break;
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        @SuppressWarnings("all")
        private void listKind(BsonWriter writer, T t, Field field) {
            String fieldName = field.getName();
            Type genericType = field.getGenericType();
            Type actual;
            if (genericType instanceof ParameterizedType) {
                ParameterizedType type = (ParameterizedType) genericType;
                actual = type.getActualTypeArguments()[0];
            } else {
                actual = String.class;
            }

            writer.writeStartArray(fieldName);

            String typeName = genericType.getTypeName();
            try {
                if (typeName.contains("List") && !typeName.contains("Array")) { // 类型为 List
                    List list = (List) field.get(t);
                    if (PACKAGES_KIND.contains(actual)) {
                        list.forEach(l -> packagesKindForList(writer, l, actual.getTypeName()));
                    } else if (actual == String.class) {
                        list.forEach(l -> writer.writeString(String.valueOf(l)));
                    } else if (DATE_KIND.contains(actual)) {
                        list.forEach(l -> dateKindForList(writer, l, actual.getTypeName()));
                    } else if (NUMBER_KIND.contains(actual)) {
                        list.forEach(l -> numberKindForList(writer, l, actual.getTypeName()));
                    } else {
                        throw new IllegalArgumentException("不支持此类型");
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

            /*if (typeName.contains("List") && !typeName.contains("Array")) {
                List list = (List) field.get(t);
                if (PACKAGES_KIND.contains(actual)) {
                    list.forEach(l -> packagesKind(writer, l, field));
                }else if (DATE_KIND.contains(actual)) { // 日期类型
                    list.forEach(l -> packagesKind(writer, l, field));
                    dateKind(writer, t, field);
                } else if (NUMBER_KIND.contains(actual)) { // 特殊数值类型
                    numberKind(writer, t, field);
                } else if (LIST_KIND.contains(actual)) { // 集合类型
                    listKind(writer, t, field);
                } else if (String.class == actual) { // 字符串类型
                    stringKind(writer, t, field);
                } else { // 自定义实体类型
                    encode(writer, t);
                }
            }*/

            writer.writeEndArray();
        }

        private void stringKind(BsonWriter writer, T t, Field field) {
            String fieldName = field.getName();
            try {
                writer.writeString(fieldName, String.valueOf(field.get(t)));
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public Class<T> getEncoderClass() {
            return clazz;
        }
    }

}
